在實務上,我們常常會實作 paging 功能,
並且使用 limit 與 offset 來完成,
一頁 10 筆 records,
略過前面 30 筆資料,到達第四頁,
像是下面這樣。
select * from letters
order by createTime DESC
limit 10 OFFSET 30
如果在資料不多的情況下,可能毫無感覺,但果今天資料量大時,可能就不一樣了。
select * from letters
order by createTime DESC
limit 10 OFFSET 400000
咦?奇怪,怎麼突然感覺翻頁越翻越慢了?
或許是沒在createTime
建立 index。CREATE INDEX ON letters (createTime);
查了一下 Explain,如果<1 萬筆,都會正常使用 index 做 query。
但>時,就又開始使用Seq Scan
。
崩潰,到底發生什麼事情了!
先來 explain 看看加完 index 後的結果
owl_conference=# EXPLAIN ANALYZE SELECT * FROM letters
ORDER BY createTime DESC OFFSET 400000 LIMIT 10;
QUERY PLAN
-------------------------------------------------------------------------
Limit (cost=23710.66..23711.25 rows=10 width=24)
(actual time=881.710..881.718 rows=10 loops=1)
-> Index Scan Backward using letters_delivered_by_idx on letters
(cost=0.42..24532.28 rows=413861 width=24)
(actual time=0.013..854.924 rows=400010 loops=1)
Planning time: 0.106 ms
Execution time: 654.810 ms
(4 rows)
可以看到雖然使用了 index 但Index Scan
還是非常的耗時啊!
我們其實只需要 10 筆 records,但他撈出了 400010 筆,這感覺是個可以突破的缺口。
注意!這個地方只對 Index Organized Table 有優化效果,
Heap Organized Table 早已經沒這個問題。
我們回想一下 Index Organized Table 的 index,
會先 query Secondary Index,然後取得 pk,
再到 Clustered Index 取出最終的 records。
所以好好利用Index Only
這個 query,直接在 index 中取值,
不要讓它到 Clustered Index 中取值。
SELECT id FROM letters
ORDER BY createTime DESC
LIMIT 10 OFFSET 400010;
可以順利地利用Index Only
直接取到 pk,然後再去 Clustered Index 中取值了。
SELECT * FROM card AS main
INNER JOIN (
SELECT id
FROM card
ORDER BY createTime DESC
LIMIT 10 OFFSET 400010
) AS sub
ON main.id = sub.id
ORDER BY createTime DESC;
雖然避開了去 Clustered Index 中取值,但 Index Only Scan 仍然是花非常多時間的,
而且如果你是 query Heap Organized Table
的 table 本來就不需要這樣做,
因為他不需要再去 Clustered Index 中取值。
OFFSET 400000 LIMIT 10
造成了(actual time=0.013..854.924 rows=400010 loops=1)
讓 Scan Index
跑了 400000 才抓到想要的內容。
何不直接使用 where 來將資料直接鎖定開頭,然候單單指抓取想要的資料呢?
變成這樣子。
SELECT ...
FROM ...
WHERE ...
AND id < ?last_seen_id
ORDER BY id DESC
FETCH FIRST 10 ROWS ONLY
我們常常會使用 createTime 作為定位點,但如果 createTime 不是 unique key,
那麼將會造成很大的困擾,可能會發生多筆 records 甚至 n 頁在同一個 createTime 上,
這時,我們必須為 index 做點手腳,使用 compose index 將 createTime 與 pk 做上綁定,
這麼一來就可以確定這個定位點是唯一值了。CREATE INDEX id_t_idx ON sales (createTime,id)
特別注意,這裡做的 compose index 順序必須要對喔!
經過優化後,來 Explain 看看。
SELECT *
FROM letters
WHERE ...
AND createTime> xxx AND id < last_seen_id
ORDER BY createTime DESC
FETCH FIRST 10 ROWS ONLY
這麼一來每次的翻頁就只會查找該段的 index 了!